<?php
/* Copyright (c) 2010 by Wade Williams  www.wadewilliams.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
 */
class formBuilder{
	private $_formElements = array();
	private $_formID;
	private $_formErrors = array();		//List of errors encountered during form validation
	private $_msgArray = array();		//Array of messages to print out incase of errors	
	private $_formSuccess;
	
	function __construct($formID=''){
		if(empty($formID))
			$this->_formID = 'builtForm';
		else $this->_formID = $formID;
		
		$this->setDefault_msgArray();
		
		if(count($this->_formElements) == 0 )
			$this->addHiddenId();
	}

	public function clean(){
		$this->runForm();
	}
	public function runForm(){
	
	//first clean the array to make sure required rules are there.
	foreach($this->_formElements as $name => $el){
		if(isset($el['properties'])){
			$properties = $el['properties'];
			if(is_array($properties) && isset($properties['required']) && $properties['required'] == true){
				if(!isset($properties['rules']))
					$newRules = 'required';
				else{
					$rules = $properties['rules'];
					if(!strstr($rules,'required'))
						$newRules = $rules . ',required';
				}
				
				//echo $newRules . '<br>';
				$this->_formElements[$name]['properties']['rules'] = $newRules;
				//echo $properties['rules'];
			}
		}	
	}
	
	if(isset($_POST['formID'])){
		if($this->checkRules()){
			$formHandler = $this->formID() . '_formHandler';
		//	echo $formHandler . '!!!<br>';
			if(function_exists($formHandler)){
				$result = $formHandler();
				if($result)
					$this->printSuccess();
				else $this->printForm();
			}
			else echo '<h1>No Form Handler Available for ' . $this->formID() . '</h1>';
		}
		else{
			//Fail
			$this->printMessages();
			$this->printForm();
		}
	}
	else 
		$this->printForm();
}

//Variable Helper Functions
	public function formID(){
		return $this->_formID;
	}
	private function setDefault_msgArray(){
		$this->_msgArray = array(
		'required' => '* Required',
		'email' => 'Please enter a valid email address',
		'alpha' => 'No numbers allowed',
		'alpha_dash' => "Only letters, - and _ allowed",
		'alpha_space' => "Letters and spaces only",
		'alpha_numeric' => "Only numbers and letters here",
		'alpha_dash_numeric' => "Letters, numbers and dashes only",
		'numeric' => "Numbers only",
		'integer' => "Integers only",
		'match' => 'This and That must match',
		'min_length' => 'Must be at least % characters',
		'max_length' => 'Maximum  length is % characters',
		'exact_length' => 'Field must be exactly % characters long'
		);
	}
	public function setMessage($rule,$message){
		$rule = strtolower(trim($rule));
		if(isset($this->_msgArray[$rule])){//a rule named like this exists
			$this->_msgArray[$rule] = $message;
		}
		else{//tell the what rules they can set messages for.
			 echo 'Rule ' . $rule . ' wasn\'t found.<br><br>Current Rules and Messages:<br>';
			 foreach($this->_msgArray as $rule => $message)
			 	echo $rule . ' -> ' . $message;
		}
	}
/**
 * 
 * Returns a plain text error message based on $name, $rule and $param
 * @param string $name
 * @param string $rule
 * @param string $param
 */
	public function getMessage($name,$rule,$param){
		//echo 'ruleid: ' . $rule . '<br>';
		$rules = $this->_msgArray;
//		var_dump($rules);
					//This That and %
		if($rule == 'match'){
			$rules[$rule] = str_ireplace('this', $this->_formElements[$name]['properties']['label'], $rules[$rule]);
			$rules[$rule] = str_ireplace('that', $this->_formElements[$param]['properties']['label'], $rules[$rule]);
		}	
		else if ($rule == 'min_length' || $rule == 'max_length' || $rule == 'exact_length')
			str_replace('%', $param, $rules[$rule]);
				
		return $rules[$rule];
		/*
		 * 'match' => $this->_formElements[$name]['properties']['label'] . ' must match ' . $this->_formElements[$param]['properties']['label'],
		'min_length' => 'Must be at least ' . $param . ' characters',
		'max_length' => 'Maximum  length is ' . $param . ' characters',
		'exact_length' => 'Field must be exactly ' . $param . ' characters long'
		
		 */
	}
	
	private function setDefault_formSuccess(){
		$this->_formSuccess = "<h1>Success</h1>";
	}
	public function setFormSuccess($html){
		$this->_formSuccess = $html;
	}
	public function formSuccess(){
		return $this->_formSuccess;
	}	

/**
 * 
 * Loads the formBuilder array from a yaml string
 * @param string yaml data $yaml
 */
	public function load($yaml){
		// use yaml parser
		require_once("spyc.php");
		$parser = new Spyc;
		$myArray = $parser->load($yaml); // get YAML formatted variable and turn into PHP array.
		$this->_formElements = $this->_formElements + $myArray;
		//var_dump($myArray); echo '<hr>';
	}

/**
 * 
 * Outputs error messages for $name and $rule, based on getMessage()
 * @param string $name 
 * @param string $rule
 */	
	private function ruleFail($name,$rule){
		$rule = $this->getRuleParams($rule);
		if(is_array($rule)){
			$myRule = $rule['rule'];
			$param = $rule['param'];
		}
		else{
			$myRule = $rule;
			$param = '';
		}
			
		array_push($this->_formErrors, $name . ' failed: ' . $this->getMessage($name, $myRule,$param) );
	}
/**
 * 
 * Breaks a string with enclosed [params] into an array(rule,param)
 * if there are no params a string is returned.
 * @param string $rule
 * 
 */
	private function getRuleParams($rule){
		if(strstr($rule,'[') && strstr($rule,']'))
		{
			//echo '<b>' . $rule . '</b>';
			$varStart = strpos($rule,'[');
			$varEnd = strpos($rule,']');
			$var = substr($rule,$varStart+1,$varEnd-$varStart-1);
			$rule = substr($rule,0,$varStart);
				
			//echo '->' . $varStart . ',' . $varEnd .'->' . $var . '=' . $rule . '<br>';
			return array('rule'=>$rule,'param'=>$var);
		}
		else return $rule;
	}
/**
 * 
 * Tests form element $name against $ruleString and Calls ruleFail() if necessary
 * @param string $name name of the form element to check rules on
 * @param string or array $ruleString are the rules to test against
 */
	private function testRules($name, $ruleString){
		if(!is_array($ruleString) && is_string($ruleString))
		$rules = explode(',',$ruleString);
		else if(is_array($ruleString)){
			$rules = $ruleString;
			$ruleString = implode(',',$rules);
		}
		/*var_dump($rules);
		 echo '<br>&lt;-$rules; ruleString-&gt;';
		var_dump($ruleString);
		echo '<br>';
		*/
		foreach($rules as $rule){ 
		//http://www.php.net/manual/en/filter.filters.php
		//http://php.net/manual/en/function.filter-var.php
		
			$data_types= array( 
	        "alpha"=>"^[a-zA-Z]+$", 
	        "alpha_dash"=>"^[a-zA-Z_-]+$",
			"alpha_space"=>"^[a-zA-Z ]+$", 
	        "alpha_numeric"=>"^[a-zA-Z0-9]+$", 
	        "alpha_dash_numeric"=>"^[a-zA-Z0-9-_]+$", 
	    ); 
	
			//echo $name . '\'s rules: ' . $rule .'<br>';
			$rule = $this->getRuleParams($rule);
			//echo $rule . '<br>';
			if(is_array($rule)){
				$myRule = $rule['rule'] . '[' . $rule['param'] . ']';//rebuild the rule in case we need to pass it to ruleFail
				switch($rule['rule']){
					case 'match':
						if($_POST[$rule['param']] != $_POST[$name])
							$this->ruleFail($name, $myRule);
						break;
					case 'min_length':
						if(strlen($_POST[$name]) < $rule['param'])
							$this->ruleFail($name, $myRule);
						break;
					case 'max_length':
						if(strlen($_POST[$name]) > $rule['param'])
							$this->ruleFail($name, $myRule);
						break;
					case 'exact_length':
						if(strlen($_POST[$name]) == $rule['param'])
							$this->ruleFail($name, $myRule);
						break;
					default:
						echo 'Param Rule Not Found: ' . $myRule . '<br>';
						break;
				}
			}
			else{
				switch($rule){
					case 'required':
						if(empty($_POST[$name]))
							$this->ruleFail($name,$rule);
						break;
					case 'email':			
						if(!filter_var($_POST[$name],FILTER_VALIDATE_EMAIL))
							$this->ruleFail($name,$rule);
						break;
					case 'alpha':
					case 'alpha_dash':
					case 'alpha_space':
					case 'alpha_numeric':
					case 'alpha_dash_numeric':
					case 'string':
					//	echo $data_types[$rule] .'<br>';
							if(!preg_match('/'.$data_types[$rule].'/',$_POST[$name]))
								$this->ruleFail($name,$rule);
						break;
					case 'numeric':
							if(!is_numeric($_POST[$name]))
								$this->ruleFail($name,$rule);
						break;
					case 'integer':
							if(!is_int($_POST[$name]))
								$this->ruleFail($name,$rule);
						break;
					default:
						echo 'Rule Not Found: ' . $rule . '<br>';
						break;
					}
			}
		}
	}
/**
 * 
 * Adds a hidden input named formID which contains the object formID()
 */
	private function addHiddenId(){
		$this->addElement('formID', 'hidden', array('value' => $this->formID()));
	}
/**
 * 
 * Returns a string of HTML to begin a new fieldset
 * @param string $legend if set, will also add the html legend tag
 */
	public function getNewFieldset($legend = ''){
		$fieldset = '
	 <fieldset>';
		if(!empty($legend))
			$fieldset .='
	  <legend>'.$legend.'</legend>';
	
		return $fieldset;
	}
/**
 * 
 * Returns HTML to end a fieldset
 */
	public function getEndFieldset(){
		return '
	 </fieldset>';
	}
/**
 * 
 * Returns HTML for a div class=clearer
 */
	public function getAddDivision(){
		return '
	 <div class="clearer"></div>';
	}
/**
 * Returns the string passed to it... for now... not called anywhere yet.
 */
	public function getAddHTML($string){
		return $string;
	}

/**
 * 
 * Adds an element to our _formElements array
 * @param string $name
 * @param string $type
 * @param array or string $properties
 */		
	public function addElement($name, $type, $properties=''){
		//$array = array(id => $name, type => $type, $properties);
	//	global $this->formElements();
		if(in_array($name,$this->_formElements)){
			echo 'Element already exists: ' . $name;
			return false;
		}
		else{
			$this->_formElements[$name] = array('type'=>$type, 'properties'=>$properties);
			return true;
		}
	}
/**
 * 
 * Steps through all places of _formElements and calls testRules() everywhere necessary
 */
	public function checkRules(){
		foreach($this->_formElements as $name => $el){
			if(isset($el['properties']) && is_array($el['properties'])){ //if properties isn't an array, this isn't a form field so don't check rules.
				if(isset($el['properties']['rules'])){
					$rules = $el['properties']['rules'];
					//echo $rules . '<br>';
					$this->testRules($name,$rules);
				}
			}
		}
		
		if(count($this->_formErrors) > 0)
			return false;
		else return true;
	}
/**
 * 
 * Outputs our completed form. Takes into account all recognized data from the array, as well as $_POST values that are set by the user.
 */
	public function printForm(){
		//var_dump($this->_formElements);
		//echo '<br>';
		$output = '';
		
		$output .= '
	<form action="" method="POST">';
		
		foreach($this->_formElements as $name => $el){
		//	var_dump($el) . '<br>';
			if(!is_array($el))//we have some markup to add.
				$output .= $el;
			else{

			//add non-form markup
			if(!isset($el['properties']) || !is_array($el['properties'])){
				switch(strtolower($el['type'])){
					case 'adddivision':
						$output .= $this->getAddDivision();
					break;
					case 'newfieldset':
						$output .= $this->getNewFieldset($el['properties']);
					break;
					case 'endfieldset':
						$output .= $this->getEndFieldset();
					break;
					case 'html':
						$output .= $el['properties'];
					break;
					default:
						$output .= '[unrecognized non-form markup]';
				}
			}
			else{//properties array is set	
				if(isset($el['properties']['required']) && $el['properties']['required'] == true){
					if(!isset($el['properties']['rules']))
						$this->_formElements[$name]['properties']['rules'] = 'required';
					else if (!strstr($el['properties']['rules'],'required'))
						$this->_formElements[$name]['properties']['rules'] .= ',required';
					$req = 'Required';
				}
				else $req = '';
				switch(strtolower($el['type'])){
					//########################################################################
					case "password":
					case "textfield":
						if(strtolower($el['type']) == 'password')
							$myType = 'password';
						else $myType = 'text';
						$output .= '
	  <label for="' . $name . '">' . $el['properties']['label'] . '</label>
	  <input type="'.$myType.'" name="' . $name . '" id="' . $name . '"';
						
						if(isset($el['properties']['maxlength']))
							$output.= ' maxlength="'.$el['properties']['maxlength'].'"';
						if(isset($el['properties']['size']))
							$output.= ' size="'.$el['properties']['size'].'"';
						
						if(isset($_POST[$name]))
							$value = $_POST[$name];
						else if(isset($el['properties']['value']))
							$value = $el['properties']['value'];
						else $value = false;
						
						if($value) $output.= ' value="'.$value.'"';
							
						$output .= '>' . $req;
						break;
					//########################################################################
					case "checkbox":
						//Show a checked box if the post is set and is true OR if post isn't set and element properties says to check it.
						if((isset($_POST[$name]) && $_POST[$name] != false) || (!isset($_POST[$name])) && true == $el['properties']['checked'])
							$checked = ' checked';
						$output .= '
	  <input type="hidden" name="'.$name.'" value="0">
	  <input type="checkbox" name="' . $name . '" value="'.$el['properties']['value'].'" id="' . $name . '"'.$checked.'>
	  <label for="' . $name . '">' . $el['properties']['label'] . '</label>' . $req;
						break;
					//########################################################################
					case "select":
						$options = '';
						if(isset($_POST[$name]))
							$selected = $_POST[$name];
						else if(isset($el['properties']['selected']))
							$selected = $el['properties']['selected'];
						else $selected = false;
						
						// loop tru values
						foreach($el['properties']['options'] as $value => $title){
							$options .= '
	   <option value="' . $value .'"';
							if($value == $selected) $options .= ' selected';
							$options .= '>' . $title . '</option>';
						}
						$output .= '
	  <label for="'.$name.'">'.$el['properties']['label'].'</label>
	  <select name="'.$name.'" id="'.$name.'">'. $options .'
	  </select>';
						break;
					//########################################################################
					case "hidden" :
						$output .= '
	  <input name="'.$name.'" type="hidden" value="'.$el['properties']['value'].'">';
						break;
					//########################################################################
					case "textarea" :
						if(isset($_POST[$name]))
							$value = $_POST[$name];
						else if(isset($el['properties']['value']))
							$value = $el['properties']['value'];
						else $value = false;
						
						$output .= '
	  <label for="'.$name.'">'.$el['properties']['label'].'</label><br>
	  <textarea name="'.$name.'" cols="'.$el['properties']['cols'].'" rows="'.$el['properties']['rows'].'" id="'.$name.'">'.$value.'</textarea>';
						break;
					//########################################################################
					case "radio":
						$output .= '<p>'.$el['properties']['label'].'</p>';
		
						//determine whether or not a button is meant to be checked (selected)
						//provide for both selected and checked syntax.
						if(isset($_POST[$name]))
							$checked = $_POST[$name];
						else if(isset($el['properties']['checked']))
							$checked = $el['properties']['checked'];
						else if(isset($el['properties']['selected']))
							$checked = $el['properties']['selected'];
						else $checked = false;
						
						foreach($el['properties']['buttons'] as $value => $title){
							$output .= '
	  <label><input type="radio" name="'.$name.'" value="'.$value.'"';
							if ($value == $checked) $output .= ' checked';
							$output .= '>'.$title.'</label>';
						}
						break;
					//########################################################################
					case "submit":
						$output .= '
	  <input type="submit" name="'.$name.'" value="' . $el['properties']['value'] . '">';
						break;
					//########################################################################				
					default:
						$output .= "[unknown formfield type.]";
						break;
				}
			}
			}
		}
		
		$output .= '
	</form>';
		
		echo $output;
	}

	public function printMessages(){
	//	var_dump($this->_formErrors);
		//echo '<hr>';
		foreach($this->_formErrors as $message){
			echo trim($message) . '<br>';
		}
	}

	private function printSuccess(){
		//If no one defined a success panel then we'll use our stock succes.
		$mySuccess = $this->formSuccess();
		if(!isset($mySuccess))
			$this->setDefault_formSuccess();
			
		echo $this->formSuccess();
	}
	
}